Pushing forward with namespaces and user types

Through this tutorial we will see many code snippets and witness their effects through the log of messages. To ease the reading, we will introduce specific blocks, to hold the log for :

They will take the form of :

This is a message from C++

This is a message from Lua

Using this, we will avoid redundant images showing consoles in order to make the tutorial easier to read.
Alright, time to proceed !

Organizing the environment with namespaces

Up till now, all variables and functions we added were defined in the global scope. This means that it would be easy to have name clashs and maybe overwrite things we would like to save. Enters the notion of namespaces.

Namespaces are exactly what they sell : a space for names. As such, with two namespaces A and B, each can have a function named foo doing something different. Addressing is then done through accessing the namespace first, and then the function.

Namespaces in an environment

Nilkins Scripts environments allow the creation of namespaces. A namespace can be seen as a sub-environment, in which new setup can be done. Let's see a quick example of a namespace creation. It goes through the environment :

nkScripts::Namespace* ns = env->setNamespace("nkTutorial") ;

Our namespace is now created and ready to be setup. For now, it doesn't offer a lot of functionalities... Let's address that problem ! The include go through :

#include <NilkinsScripts/Environments/Namespaces/Namespace.h>

Now, let's say we want to add a function to the namespace :

nkScripts::Function* func = ns->setFunc("foo") ; func->addParameter(nkScripts::FUNCTION_PARAMETER_TYPE::STRING) ; func->setFunction ( [&logger] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { logger->log(stack[0]._valString, "foo") ; return nkScripts::OutputValue::VOID ; } ) ;

The namespace is addressed like an Environment would. We set the function on it, and prepare the Function object to fit our needs.
In this case, it means adding a string parameter to be taken from it, and setting up the callback function to log the string given.
We can notice two new things there :

Here, the process is simple : we capture a reference over the logger from our main scope, read the parameter given and log it. No result is needed, so we simply return VOID.
So, now our function is wired to C++. What is left is trying to access it through the execution of a script, which sources will be :

nkTutorial.foo("Calling from Lua !") ;
[foo] Calling from Lua !

So now, we call foo from the namespace nkTutorial, as we defined through the C++ API. In the console, we can see that the C++ lambda given is indeed called !

As mentioned earlier, namespaces can be considered as sub environments. They allow setups very similar to environments, within their own scope. I invite you to go to the API documentation to get more information on them.

Tips

C++

To create functions or user types (introduced in next section), it is currently possible to take a shortcut regarding namespaces, by constructing the full name identifier before setting it in the environment.
For instance, a construction done like :

nkScripts::Function* func = env->setFunc("nkTutorial::test::foo") ;

Will create the foo function within the namespaces nkTutorial and test. If need be, the namespaces will be created within the environment, else they will be reused.

Lua

Within Lua, it is good practice to alias into local registers variables hidden within namespaces. For instance, before a loop using the square root function often, it is a good practice to :

local sqrt = math.sqrt ;

This can tremendously speedup the execution depending on the loop, as Lua doesn't need to go through the namespace over and over again.
Spoiler alert : this is something also advised with namespaces created through nkScripts, as the logic remains the same.
As such, let's say we have a function bar we defined through namespace nkTutorial, before a loop or some intense processing :

local bar = nkTutorial.bar ;

This will help with Lua performances, so don't forget to try it whenever it can be done !

Enters custom user types

Up till now, we have only seen functions, or basic types like strings or numbers. However, C++ features something very interesting : objects and classes.
Within nkScripts, corresponding API is called UserType.

A user type can be seen as a class. You define a type, setup the methods on it and it becomes available as you described, with type checks and all !

Basics for a user type

So let's say we have a class defined in our code. Named Data, as we were not very inspired :

class Data { public : std::string _label = "I" ; int _i = 0 ; void printLabel () { std::cout << _label << " : " << std::to_string(_i) << std::endl ; } } ;

Something cool would be to be able to wrap such an object within our scripting environment and use it like we would in C++. Luckily, user types are meant for this !

Setup a basic user type interface

To specify a user type, request its creation to the environment itself. Note that it can also use the namespace trick described earlier :

nkScripts::UserType* type = env->setUserType("nkTutorial::Data") ;

Now we have a user type declared, within the nkTutorial namespace.
Next step will be to add some methods to it. So let's do so by first including :

#include <NilkinsScripts/Environments/UserTypes/UserType.h>

And creating a method :

nkScripts::Function* method = type->addMethod("printLabel") ; method->setFunction ( [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // A method will always have the object it's called from as the first parameter in the stack Data* object = (Data*)stack[0]._valUser._userData ; object->printLabel() ; return nkScripts::OutputValue::VOID ; } ) ;

Creating a method is the same as creating a function, apart from the fact we create it right on the UserType.
One difference though : the parameter slot 0 within the stack will always be the object the function has been called on. Other potential parameters will start at slot 1.

In this function, we "proxy" the call to the method. First, cast the user data pointer, and then call the method before returning VOID.
Note that there is no way of calling directly a method within a class from a user type. A method always has to go through a global scope / static / lambda proxy.
This allows the method to be flexible enough to be called with different instances of one class.

Now, let's assume we want to use it within Lua. We need two things :

  1. An object to call it on
  2. The calling logic

So first, let's create an object from C++, and pass it to the environment :

Data data ; env->setObject("data", "nkTutorial::Data", &data) ;

We now have an object data within the environment, pointing to the instance currently allocated on the stack.
We specified its type as being the one for our user type, with the full identifier. This is important to let the environment identify variables and do type checks and method indexing.
And now, we can use it directly within a script :

data:printLabel() ; -- Or : nkTutorial.Data.printLabel(data) ; -- But version demonstrated is more robust as Lua will warn you if data is nil
I : 0

What we did is call the printLabel function on data. This went all the way to C++ where the callback forwarded the call to the object's method.

Construction and destruction within an environment

For last example, what we did was feed the object from C++ to the environment. There is an alternative : describe how the user type should be created from the scripting environment.

For that, we will need a pair of function : the constructor, and the destructor. Let's see how we can specify them :

type->setConstructor ( [] (const nkScripts::DataStack& stack) -> void* { return new Data () ; } ) ; type->setDestructor ( [] (void* data) { delete (Data*)data ; } ) ;

What we did is :

  1. Set the constructor of the type, as a function returning a void*, being the new allocation for our object
  2. Set the destructor of the type, casting the object before calling delete on it

Constructor will be mapped to a static method on the type, called new. Now, it means that within Lua, we can :

local d = nkTutorial.Data.new() ; d:printLabel() ;

We are creating a Data object right from Lua by querying the constructor callback. After that, it is possible to call any method on the user type, as they will be wired within C++.

Object ownership

One very important aspect of sharing memory between C++ and scripting environments is ownership. Being owner means that one environment (script or C++) is responsible for freeing the memory when the object is not needed anymore. There are some easy cases :

Within nkScripts, when C++ is the owner of one object, the scripting environment won't do anything to an object when its last reference is garbage collected.
Alternatively, if it is the owner, it will call the destructor callback.
This needs to be taken with care as freeing something still used within either of the environments can cause segmentation faults. It can even happen with some delay, as garbage collection happens... When it happens.

When the ownership is not easily deduced, the nkScripts component offers clear API to request it. For instance, let's introduce a new function within the user type :

nkScripts::Function* staticMethod = type->addStaticMethod("duplicate") ; staticMethod->addParameter(nkScripts::FUNCTION_PARAMETER_TYPE::USER_DATA_PTR, "nkTutorial::Data") ; staticMethod->setFunction ( [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // Cast the input Data* input = (Data*)stack[0]._valUser._userData ; Data* result = new Data () ; result->_label = input->_label + "d" ; result->_i = input->_i + 1 ; // By returning true, we mean that the script is to be considered owner of the memory we are returning return nkScripts::OutputValue(result, "nkTutorial::Data", true) ; } ) ;

Some new aspects for the declaration :

This boolean is important : it encodes whether yes or no, the scripting environment has to consider itself as owner of the object returned.
In this context, because the duplicate method will be called from the script, allocation will happen for the script itself. We request the scripting environment to take ownership of the object. This will cause the destructor callback to be called whenever last reference over it will be dropped.
For instance, a case where we would not want the scripting environment to take ownership is when accessing a member of an existing class. Here, we are not allocating anything.

Now that we defined this function, let's see how we can use it within Lua :

local d = nkTutorial.Data.duplicate(data) ; d:printLabel() ;
Id : 1

A static method is callable from its full name identifier, like in C++.
When using it on d, we can notice that we duplicated the data object we fed from C++, while indeed altering slightly its members, causing the output we now have.

General limitations for functions and methods

There are some aspects of C++ that cannot be reproduced through the API :

Conclusion

With this, we saw all the basics for a good organization of an environment, concluding this tutorial.

Through namespaces, create sub-environments in which local declaration will be independent from the others.

Through user types, describe classes and enable inter-operability with C++'s object paradigm.

This tutorial was maybe a little dense, especially with all the memory ownership problematic. However, nkScripts tries its best to make it clear and reduce the number of times it is required to think about it. Through next tutorials, we will push a little bit those aspects. More control over memory is possible as we will see. We will also talk about the ways available within user types to address their components through indexing, fields... Helping in making the scripting accesses more... Scripty !